from inspect import getargspec
from django.template import Context
from django.template.loader import get_template
from namedquerysets import ProgrammerError

class IllegalValue(Exception):
    pass

def new_filter(filter_class, slug, name, reset=['page']):
    """
    Convenience method for calling a filter's constructor and
    returning the instance for method chaining.
    """
    return filter_class(slug, name, reset)
    
class AbstractQuerysetFilter(object):
    def __init__(self, slug, name, reset=['page']):
        """
        slug - an ascii string. Should be unique within a namedqueryset.
        name - A (possibly unicode) string, for presentation.
        reset - a list of ascii strings, explained as follows:
            If there are any parameters (probably unrelated to this filter)
            which you'd like to remove from the querystring every time one
            of the parameters from this filter changes, then this is the place
            to list them. (Default: ['page'])
        
        Sub-classes should not over-ride this method.
        """
        self.slug = slug
        self.display_name = name
        self.reset_params = reset
        self.param_name_map = {}
        self.reverse_name_map = {}
        assert (getargspec(type(self)._filter) ==
                getargspec(type(self)._validate_params) ==
                getargspec(type(self)._context)), \
                """
                Sub-classes of AbstractQueryFilter must properly define
                these three methods: _filter, _context, _validate_params.
                %r didn't.
                """ % type(self)
    
    def _validate_params(self, queryset, param_list_goes_here):
        """
        Returns true if and only if every parameter has an expected
        and acceptable value. Implementers should be as strict as
        possible.
        
        Sub-classes MUST over-ride this method. Also, they should
        provide explicit parameter names (don't use wildcard *args
        or **kwargs). The first two parameters must be called
        'self' and 'queryset'.
        """
        raise NotImplementedError
    
    def _filter(self, queryset, param_list_goes_here):
        """
        Returns a queryset.
        
        Sub-classes MUST over-ride this method. Also, they should
        provide explicit parameter names (don't use wildcard *args
        or **kwargs). The first two parameters must be called
        'self' and 'queryset'.
        """
        raise NotImplementedError
    
    def _context(self, queryset, param_list_goes_here):
        """
        Returns a dictionary of extra context for use in rendering
        the filter's UI template.
        
        Sub-classes should over-ride this method. Also, they should
        provide explicit parameter names (don't use wildcard *args
        or **kwargs). The first two parameters must be called
        'self' and 'queryset'.
        """
        return {}
    
    def name(self):
        return self.display_name
        
    def filter(self, queryset, params):
        """
        Returns a filtered queryset, or raises an IllegalValue exception if
        the filter's parameter values provided in the context were invalid.
        
        queryset - a django.db.models.query.QuerySet
        context - a dict
        
        (Child classes MUST NOT over-ride this method, and
        instead should over-ride _validate_params() and _filter().)
        """
        params = self._unalias_params(params)
        if self._validate_params(queryset, **params):
            return self._filter(queryset, **params)
        else:
            raise IllegalValue,"Bad parameters for filter %s" % (self.name(),)

    def _unalias_params(self, params):
        result = {}
        for external_name,internal_name in self.param_name_map.iteritems():
            #work-around a bug in django Context's "in" operator.
            try:
                result[internal_name] = params[external_name]
            except KeyError:
                pass
        return result

    def params(self):
        """
        Returns a dict, mapping from internal to external param names.
        internal params that haven't been given an external name will
        not be listed as keys in the result of this method.
        
        See set_param_names().
        """
        return self.reverse_name_map
        
    def set_param_names(self,**kwargs):
        """
        Give names to this filter's parameters.
        
        During filtering, the request context and GET parameters will be
        searched for these names, and their corresponding values applied to
        the filter. Note that any filter parameters left un-named here will
        not be exposed (for dynamic setting) during filtering.
        
        An exception will be thrown here if you:
         - Name a parameter that isn't listed in your _filter() function
         - Name the queryset parameter.
         - Give the same name to more than one parameter.
         - Provide a name that fails the following: len(str(name)) > 0.
        
        Returns self.
        
        Example:
           filter.set_param_names(name='n',quest='q',favorite_color='color')
        
        """
        
        try:
            (args,varargs,varkw,defaults) = getargspec(type(self)._filter)
            filter_params = args[1:]
        except:
            raise ProgrammerError, "Introspection failed!"
        
        self.param_name_map = {}
        self.reverse_name_map = {}
        for internal_name, external_name in kwargs.iteritems():
            try:
                external_name = str(external_name)
                assert (len(external_name) > 0)
            except:
                raise ProgrammerError,"Invalid name for %s" % (internal_name,)
            
            if external_name in self.param_name_map:
                raise ProgrammerError,"Duplicate name: %s" % (external_name,)
            
            if external_name == 'queryset':
                raise ProgrammerError,"Don't expose the queryset!"
            
            if internal_name not in filter_params:
                raise ProgrammerError, \
                      "_filter() has no such param: %s" % (internal_name,)
            
            self.param_name_map[external_name] = internal_name
            self.reverse_name_map[internal_name] = external_name
        return self


class AbstractSingleChoiceFilter(AbstractQuerysetFilter):
    
    default_template_name = "single_choice_filter.html"
    
    def _validate_params(self, queryset, choice=None):
        """
        Sub-classes needn't over-ride this method. Instead they
        should override the _filter() and choices() methods.
        """
        for ch in self.choices(queryset):
            if str(choice) == str(ch['value']):
                return True
        return False
    
    def _filter(self, queryset, choice=None):
        raise NotImplementedError
    
    def _context(self, queryset, choice=None):
        return {'choices': self.choices(queryset)}
    
    def choices(self, queryset):
        """
        Returns or yields a sequence of {'display':x,'value':y) options.
            display - a (possibly unicode) string, for presentation
            value - a (possibly unicode) string, for representation
        """
        raise NotImplementedError

class AbstractStaticSingleChoiceFilter(AbstractSingleChoiceFilter):
    """
    Sub-classes MUST define a class variable 'options_and_functions' which is
    a sequence of (display,value,func) option triplets:
        display - a (possibly unicode) string, for presentation
        value - a (possibly unicode) string, for representation
        func - a function that takes a QuerySet and returns a QuerySet.
    """
    
    def _filter(self, queryset, choice=None):
        """
        Sub-classes should not over-ride this method. See the class docstring.
        """  
        if hasattr(self, 'options_and_functions'):
            for display,value,func in type(self).options_and_functions:
                if value == choice:
                    return func(queryset)
            else: #exhausted list
                assert(False, "Bad value for choice that passed validation.")
        else:
            raise NotImplementedError
        
    def choices(self, queryset):
        """
        Sub-classes should not over-ride this method. See the class docstring.
        """  
        if hasattr(self, 'options_and_functions'):
            for display,value,func in type(self).options_and_functions:
                yield {'display': str(display), 'value': str(value)}
        else:
            raise NotImplementedError

class OrderByFilter(AbstractQuerysetFilter):
    """
    Example:
      filter = OrderByFilter("Order By")
      filter.name_params(order="order")
      filter.set_orderings(
          {'display':"Name",  'ordering':"name",
           'order_by':["name"]},
          {'display':"Color", 'ordering':"color",
           'order_by':["favorite_color","name"]},
          {'display':"Quest", 'ordering':"quest",
           'order_by':["quest","-creation_date"]},
      )
                          
    Example Results:
        Context             QuerySet's order_by
        -------------       -----------------------------
        order=color         ['favorite_color','name']
        order=-quest        ['-quest','-creation_date']
        
    Note that if the queryset being filtered already has an order_by,
    then the order_by specified by this filter will be *appended* to the
    queryset's existing order_by list. This is to allow independently
    adjustable secondary orderings.
    """
    
    default_template_name = "order_by_filter.html"
    
    def __init__(self, *args, **kwargs):
        super(OrderByFilter,self).__init__(*args, **kwargs)
        self.orderings = {}
        self.columns = ()
        
    def _validate_params(self, queryset, order=''):
        return order == '' or order in self.orderings
    
    def _context(self, queryset, order=''):
        ascending = False
        descending = False
        if order:
            if order.startswith("-"):
                descending = True
                order = order[1:]
            else:
                ascending = True
        return {'ascending': ascending,
                'descending': descending,
                'ordering': order}
    
    def _filter(self, queryset, order=''):
        if order:
            order_by = self.orderings[order]
        else:
            order_by = []
        if queryset._order_by:
            order_by = list(queryset._order_by) + list(order_by)
        if order_by:
            return queryset.order_by(*order_by)
        else:
            return queryset
        
    def set_orderings(self, orderings):
        """
        See class docstring for a code example.
        
        orderings - a sequence of dicts with the following keys:
            display - the UI column name, for presentation.
            ordering - the ordering ID, a string which musn't begin with a dash.
            order_by - a list suitable for passing to QuerySet.order_by().
        """
        #TODO: validate that orderings is well-formed.
        def flipdash(ns):
            n,s = ns
            if n > 0: return s
            if s.startswith("-"): return s[1:]
            else: return "-" + s
        
        self.orderings = {}
        self.columns = []
        for o in orderings:
            display = o['display']
            ordering = o['ordering']
            reversed = "-" + ordering
            order_by = o['order_by']
            assert(not ordering.startswith("-"))
            self.columns.append({'display':display,
                                 'ordering':ordering,
                                 'reversed':reversed})
            self.orderings[ordering] = order_by
            self.orderings[reversed] = map(flipdash, enumerate(order_by))
        self.columns = tuple(self.columns)
        return self
        
class SearchFilter(AbstractQuerysetFilter):
    
    default_template_name = "search_filter.html"
    
    def _validate_params(self, queryset, search_string=''):
        return isinstance(search_string,basestring)
    
    def _context(self, queryset, search_string=''):
        return {}
    
    def _filter(self, queryset, search_string=''):
        from exofulltext import djangosearch
        if search_string:
            queryset = djangosearch.search(search_string,queryset)
        return queryset

class AbstractGroupFilter(AbstractQuerysetFilter):
    
    default_template_name = "group_filter.html" #TODO
    
    INTERSECT = "I"
    UNION = "U"

    def _validate_params(self, queryset, groups=[], setlogic=UNION):
        """
        Sub-classes should not over-ride this method, and instead
        should implement the groups() and _filter_group() methods.
        """
        if setlogic not in (UNION, INTERSECT): return False
        if isinstance(groups,basestring): groups = [groups]
        for group in groups:
            if group not in (id for (n,id,count) in self.groups(queryset)):
                return False
        return True
    
    def _filter(self, queryset, groups=[], setlogic=UNION):
        """
        Sub-classes should not over-ride this method, and instead
        should implement the groups() and _filter_group() methods.
        """
        if isinstance(groups,basestring): groups = [groups]
        if not groups: return queryset
        for group in groups:
            if setlogic == INTERSECT:
                queryset = queryset & self._filter_group(queryset,group)
            elif setlogic == UNION:
                queryset = queryset | self._filter_group(queryset,group)
            else:
                raise ProgrammerError
        return queryset
    
    def _context(self, querysert, groups=[], setlogic=UNION):
        return {'group_list': self.groups(querysert)}
    
    def _filter_group(self, queryset, group):
        """
        Filters the queryset so that only objects in the group are included.
        """
        raise NotImplementedError
        
    def groups(self, queryset):
        """
        Returns an ordered list of (group_name,group_id,count) triplets.
        """
        raise NotImplementedError
